%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% texteditor.sl
%
% This script implements a basic text editor (something on the level of
% Microsoft Notepad) using SLgtk and SLglade.  It is intended as an
% advanced example of how to use SLglade.
%
% The interface is generated with glade_xml_new() from the Glade XML file
% "texteditor.glade".  All signal handlers are specifed in the XML and
% connected by glade_xml_signal_autoconnect().
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


import("gtk");
import("glade");


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Global variables
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


% Bundle all global data into the "data" structure
variable data = struct {
   xml,		% GladeXML object for interface
   filename,	% Name of file currently being edited
   clipboard	% Clipboard contents
};


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Function definitions
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


define get_text()
% Returns string containing text in TextDisplay.
{
   variable textview = glade_xml_get_widget(data.xml,"TextDisplay");
   variable textbuff = gtk_text_view_get_buffer(textview);
   variable text_start = gtk_text_buffer_get_start_iter(textbuff);
   variable text_end = gtk_text_buffer_get_end_iter(textbuff);

   return gtk_text_buffer_get_text(textbuff,text_start,text_end,0);
}

define set_text(textstr)
% Sets text in TextDisplay to contents of "textstr".
{
   variable textview = glade_xml_get_widget(data.xml,"TextDisplay");
   variable textbuff = gtk_text_view_get_buffer(textview);

   gtk_text_buffer_set_text(textbuff,textstr,-1);
}

define set_font(arg)
% Changes font in TextDisplay to that specified by FontSelectionDialog.  This
% function is specified in "texteditor.glade" as the handler for the
% "clicked" signal on FontSelectionDialog's "Apply" and "OK" buttons.
% The argument is not used.
{
   variable font_select = glade_xml_get_widget(data.xml,"FontSelectionDialog");
   variable font = gtk_font_selection_dialog_get_font_name(font_select);
   variable font_desc = pango_font_description_from_string(font);

   variable text_display = glade_xml_get_widget(data.xml,"TextDisplay");
   gtk_widget_modify_font(text_display,font_desc);
}

define show_error_dialog(parent_name,msg)
% Opens ErrorDialog on top of the widget whose name is given by
% "parent_name".  "msg" contains the error message to display.
{
   variable parent = glade_xml_get_widget(data.xml,parent_name);
   variable error_dialog = glade_xml_get_widget(data.xml,"ErrorDialog");
   variable error_message = glade_xml_get_widget(data.xml,"ErrorMessage");

   gtk_label_set_text(error_message,msg);
   gtk_window_set_transient_for(error_dialog,parent);
   () = gtk_dialog_run(error_dialog);
   gtk_widget_hide(error_dialog);
}

define is_directory(filename)
% Returns 1 if "filename" is the name of a directory, 0 otherwise.
{
   variable st = stat_file(filename);
   if (st == NULL)  return 0;
   return stat_is("dir",st.st_mode);
}

define open_file(filename)
% Opens file specified by "filename" and places its contents in TextDisplay.
% Will open an error dialog on failure.
{
   variable fp = fopen(filename,"r");

   % If we can't open file, issue error and quit
   if (fp == NULL) {
      show_error_dialog("FileSelect",sprintf("Unable to open file\n%s",filename));
      return;
   }

   % Store all lines of file in "text"

   variable text = "";
   variable line;

   while (-1 != fgets(&line,fp))  text = strcat(text,line);
   () = fclose(fp);

   % Display text and store filename
   set_text(text);
   data.filename = filename;

   % Mark buffer as unmodified
   variable textview = glade_xml_get_widget(data.xml,"TextDisplay");
   variable textbuff = gtk_text_view_get_buffer(textview);
   gtk_text_buffer_set_modified(textbuff,0);
}

define save_file(filename)
% Saves current contents of TextDisplay to file specified by "filename".
% Will open an error dialog on failure.
{
   variable fp = fopen(filename,"w");

   % If unable to open or write to file, issue error and quit
   if (orelse { fp == NULL } { -1 == fputs(strcat(get_text(),"\n"),fp) }) {
      show_error_dialog("FileSelect",sprintf("Unable to save to file\n%s",filename));
      return;
   }

   % Store filename
   data.filename = filename;

   % Mark buffer as unmodified
   variable textview = glade_xml_get_widget(data.xml,"TextDisplay");
   variable textbuff = gtk_text_view_get_buffer(textview);
   gtk_text_buffer_set_modified(textbuff,0);
}

define get_selected_text(delete_text)
% Returns string containing currently selected text in TextDisplay,
% or NULL if no text is selected.  If "delete_text" evaluates to true,
% the selected text will also be deleted.
{
   variable textview = glade_xml_get_widget(data.xml,"TextDisplay");
   variable textbuff = gtk_text_view_get_buffer(textview);

   % We need two GtkTextIter's; it really doesn't matter which ones,
   % as they'll be modified by gtk_text_buffer_get_selection_bounds()
   variable text_start = gtk_text_buffer_get_start_iter(textbuff);
   variable text_end = gtk_text_buffer_get_end_iter(textbuff);

   variable text;

   !if (gtk_text_buffer_get_selection_bounds(textbuff,text_start,text_end))
      % No text selected, so return NULL
      return NULL;
   else
      % Store selected text in "text"
      text = gtk_text_buffer_get_text(textbuff,text_start,text_end,0);

   % If requested, delete selected text
   if (delete_text)  gtk_text_buffer_delete(textbuff,text_start,text_end);

   return text;
}

define on_cut1_activate(arg)
% Autoconnected handler for "Edit->Cut" menu item.
% The argument is not used.
{
   variable text = get_selected_text(1);
   if (text != NULL)  data.clipboard = text;
}

define on_copy1_activate(arg)
% Autoconnected handler for "Edit->Copy" menu item.
% The argument is not used.
{
   variable text = get_selected_text(0);
   if (text != NULL)  data.clipboard = text;
}

define on_paste1_activate(arg)
% Autoconnected handler for "Edit->Paste" menu item.
% The argument is not used.
{
   if (data.clipboard == NULL)  return;

   variable textview = glade_xml_get_widget(data.xml,"TextDisplay");
   variable textbuff = gtk_text_view_get_buffer(textview);

   gtk_text_buffer_insert_at_cursor(textbuff,data.clipboard,-1);
}

define on_delete1_activate(arg)
% Autoconnected handler for "Edit->Delete" menu item.
% The argument is not used.
{
   () = get_selected_text(1);
}

define get_file_select_response(title)
% Runs the FileSelect dialog with "title" as the window title and
% returns the response signal from gtk_dialog_run() and the selected
% file name.  Note that it will not return GTK_RESPONSE_OK while the
% selected file is a directory (i.e. clicking the "OK" button will not
% close the dialog unless a non-directory file is selected).
{
   variable main_window = glade_xml_get_widget(data.xml,"MainWindow");
   variable file_select = glade_xml_get_widget(data.xml,"FileSelect");

   gtk_window_set_title(file_select,title);
   gtk_window_set_transient_for(file_select,main_window);

   variable filename;

   % If we're editing a named file, set the working directory of FileSelect
   % to the directory containing the file; otherwise, start in the current
   % working directory
   if (data.filename != NULL)
      filename = path_dirname(data.filename);
   else
      filename = getcwd();

   variable response;

   % FileSelect will stay open until the user closes it via the window manager,
   % clicks "Cancel", or clicks "OK" with a non-directory file selected.  If
   % the user clicks "OK" with a directory selected, the working directory of
   % FileSelect will change to that directory, but the dialog will stay open.
   do {
      % Use path_concat() to make sure there's a trailing slash; this is necessary
      % in order to set a working directory
      gtk_file_selection_set_filename(file_select,path_concat(filename,""));
      response = gtk_dialog_run(file_select);
      filename = gtk_file_selection_get_filename(file_select);
   } while (andelse {response == GTK_RESPONSE_OK} {is_directory(filename)});

   return (response,filename);
}

define get_save_file_response()
% Runs the FileSelect dialog with the window title set to "Save File"
% and returns the response signal from gtk_dialog_run() and the selected
% file name.  If the user selects a file that already exists, it will
% open OverwriteFileDialog to ask for confirmation.
{
   variable file_select = glade_xml_get_widget(data.xml,"FileSelect");
   variable filename;
   variable response;
   variable overwrite_response;

   do {
      overwrite_response = GTK_RESPONSE_YES;
      (response,filename) = get_file_select_response("Save File");

      if (response == GTK_RESPONSE_OK) {
	 % If selected file already exists, get confirmation that
	 % it's okay to overwrite
         if (stat_file(filename) != NULL) {
            variable overwrite_dialog = glade_xml_get_widget(data.xml,"OverwriteFileDialog");
            gtk_window_set_transient_for(overwrite_dialog,file_select);
            overwrite_response = gtk_dialog_run(overwrite_dialog);
            gtk_widget_hide(overwrite_dialog);
	 }
      }
   } while (overwrite_response != GTK_RESPONSE_YES);

   gtk_widget_hide(file_select);
   return (response,filename);
}

define abort_after_checking_buffer()
% This function is used to prevent another routine from erasing unsaved
% changes to the current file without the user's consent.  It returns 0
% if the calling routine should continue or 1 if it should abort.
{
   variable textview = glade_xml_get_widget(data.xml,"TextDisplay");
   variable textbuff = gtk_text_view_get_buffer(textview);

   % If buffer is unmodified, signal not to abort
   !if (gtk_text_buffer_get_modified(textbuff))  return 0;

   % Ask user if changes to buffer should be saved
   variable main_window = glade_xml_get_widget(data.xml,"MainWindow");
   variable save_changes_dialog = glade_xml_get_widget(data.xml,"SaveChangesDialog");
   gtk_window_set_transient_for(save_changes_dialog,main_window);
   variable response = gtk_dialog_run(save_changes_dialog);
   gtk_widget_hide(save_changes_dialog);

   if (response == GTK_RESPONSE_NO) {
      % User clicked "No", so don't save changes and signal not to abort
      return 0;
   } else if (response == GTK_RESPONSE_YES) {
      % User clicked "Yes"

      % If text is already associated with a file, save changes to that
      % file and signal not to abort
      if (data.filename != NULL) {
         save_file(data.filename);
	 return 0;
      }

      % Text has not been saved previously, so prompt the user for a
      % file to save to
      variable filename;
      (response,filename) = get_save_file_response();

      % If the user clicked "OK", save to the selected file and
      % signal not to abort
      if (response == GTK_RESPONSE_OK) {
         save_file(filename);
	 return 0;
      }
   }

   % If we reached here, it means we should signal to abort
   return 1;
}

define on_new1_activate(arg)
% Autoconnected handler for "File->New" menu item.
% The argument is not used.
{
   if (abort_after_checking_buffer())  return;
   set_text("");
   data.filename = NULL;
}

define on_open1_activate(arg)
% Autoconnected handler for "File->Open" menu item.
% The argument is not used.
{
   if (abort_after_checking_buffer())  return;

   variable file_select = glade_xml_get_widget(data.xml,"FileSelect");
   variable response;
   variable filename;

   (response,filename) = get_file_select_response("Open File");
   if (response == GTK_RESPONSE_OK)  open_file(filename);
   gtk_widget_hide(file_select);
}

define on_save_as1_activate(arg)
% Autoconnected handler for "File->Save As" menu item.
% The argument is not used.
{
   variable file_select = glade_xml_get_widget(data.xml,"FileSelect");
   variable filename;
   variable response;

   (response,filename) = get_save_file_response();
   if (response == GTK_RESPONSE_OK)  save_file(filename);
}

define on_save1_activate(arg)
% Autoconnected handler for "File->Save" menu item.
% The argument is not used.
{
   if (data.filename == NULL)
      % Text is not associated with a file, so go to "Save As"
      on_save_as1_activate(arg);
   else
      % Text is already associated with a file, so save to that file
      save_file(data.filename);
}

define on_quit1_activate(arg)
% Autoconnected handler for "File->Quit" menu item.
% The argument is not used.
{
   if (abort_after_checking_buffer())  return;
   gtk_main_quit();
}

define on_main_window_delete(arg1,arg2)
% Autoconnected handler for the "delete_event" signal on MainWindow.
% (This gets called when the user tries to close the window via the
% window manager.)  The arguments are not used.
{
   on_quit1_activate(NULL);

   % It seems that we have to return a value from this function, but
   % I don't know what it's supposed to indicate
   return 1;
}

define on_word_wrap1_activate(arg)
% Autoconnected handler for "Format->Word Wrap" menu item.  (Toggles
% word wrap setting.)  The argument is not used.
{
   variable text_display = glade_xml_get_widget(data.xml,"TextDisplay");

   if (gtk_text_view_get_wrap_mode(text_display) == GTK_WRAP_WORD)
      gtk_text_view_set_wrap_mode(text_display,GTK_WRAP_NONE);
   else
      gtk_text_view_set_wrap_mode(text_display,GTK_WRAP_WORD);
}

define on_font1_activate(arg)
% Autoconnected handler for "Format->Font" menu item.  Note that set_font()
% is set as the handler for the "clicked" signal on FontSelectionDialog's
% "Apply" and "OK" buttons in "texteditor.glade", so it isn't called here.
% The argument is not used.
{
   variable main_window = glade_xml_get_widget(data.xml,"MainWindow");
   variable font_dialog = glade_xml_get_widget(data.xml,"FontSelectionDialog");

   gtk_window_set_transient_for(font_dialog,main_window);

   % The dialog should stay open when the user clicks "Apply"
   variable response;
   do {
      response = gtk_dialog_run(font_dialog);
   } while (response == GTK_RESPONSE_APPLY);

   gtk_widget_hide(font_dialog);
}

define on_about1_activate(arg)
% Autoconnected handler for "Help->About" menu item.
% The argument is not used.
{
   variable main_window = glade_xml_get_widget(data.xml,"MainWindow");
   variable about_dialog = glade_xml_get_widget(data.xml,"AboutDialog");

   gtk_window_set_transient_for(about_dialog,main_window);
   () = gtk_dialog_run(about_dialog);
   gtk_widget_hide(about_dialog);
}


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Main program
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


% Create the interface, connect the signal handlers, and run the program
data.xml = glade_xml_new("texteditor.glade");
glade_xml_signal_autoconnect(data.xml);
gtk_main();

% For debugging purposes: if anything is still on the stack (e.g.
% function arguments that we forgot to deal with), print stack contents
if (_stkdepth()) {
   message("DEBUGGING: Contents of stack:");
   _print_stack();
}

